<?php
declare(strict_types=1);

namespace Core\HyperfUnit;

use Core\Exception\PrintException;
use JetBrains\PhpStorm\ArrayShape;

/**
 * 请求数据安全扫描
 * @method self setWebscanSwitch(bool $switch)  是否开启扫描
 * @method self setWebscanGet(bool $switch)     是否开启GET数据扫描
 * @method self setWebscanPost(bool $switch)    是否开启POST数据扫描
 * @method self setWebscanCookie(bool $switch)  是否开启COOKIE数据扫描
 */
class Webscan
{
    /**
     * 是否开启检测
     * @var bool|mixed
     */
    private bool $webscan_switch = true;//检测开关

    /**
     * 是否扫描GET数据
     * @var bool
     */
    private bool $webscan_get = true;

    /**
     * 是否扫描POST数据
     * @var bool
     */
    private bool $webscan_post = true;

    /**
     * 是否扫描COOKIE
     * @var bool
     */
    private bool $webscan_cookie = true;


    /**
     * @param string $name
     * @param array $arguments
     * @return $this
     */
    public function __call(string $name, array $arguments)
    {
        switch ($name) {
            case 'setWebscanSwitch':
                $this->webscan_switch = $arguments[0];
                break;
            case 'setWebscanGet':
                $this->webscan_get = $arguments[0];
                break;
            case 'setWebscanPost':
                $this->webscan_post = $arguments[0];
                break;
            case 'setWebscanCookie':
                $this->webscan_cookie = $arguments[0];
                break;
        }
        return $this;
    }

    /**
     * 获取实例
     * @return Webscan
     */
    public static function instance(): Webscan
    {
        return new self();
    }


    /**
     * 检测数据
     * @param array $param
     * @return bool
     */
    public function check(#[ArrayShape(['get' => 'array', 'post' => 'array', 'cookie' => 'array'])] array $param): bool
    {
        //未开启扫描直接跳过
        if ($this->webscan_switch === false) {
            return true;
        }
        // get拦截规则
        $getfilter = "\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|\\b(group_)?concat[\\s\\/\\*]*?\\([^\\)]+?\\)|\bcase[\s\/\*]*?when[\s\/\*]*?\([^\)]+?\)|load_file\s*?\\()|<[a-z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
        // post拦截规则
        $postfilter = "<.*=(&#\\d+?;?)+?>|<.*data=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|\\b(group_)?concat[\\s\\/\\*]*?\\([^\\)]+?\\)|\bcase[\s\/\*]*?when[\s\/\*]*?\([^\)]+?\)|load_file\s*?\\()|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
        // cookie拦截规则
        $cookiefilter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";


        if ($this->webscan_get && isset($param['get']) && $param['get']) {
            foreach ($param['get'] as $key => $value) {
                if ($this->webscanStopAttack($key, $value, $getfilter) === false) {
                    return false;
                }
            }
        }
        if ($this->webscan_post && isset($param['post']) && $param['post']) {
            foreach ($param['post'] as $key => $value) {
                if ($this->webscanStopAttack($key, $value, $postfilter) === false) {
                    return false;
                }
            }
        }
        if ($this->webscan_cookie && isset($param['cookie']) && $param['cookie']) {
            foreach ($param['cookie'] as $key => $value) {
                if ($this->webscanStopAttack($key, $value, $cookiefilter) === false) {
                    return false;
                }
            }
        }
        return true;
    }


    /**
     * 参数拆分
     * @param mixed $arr
     * @param bool $init
     * @return string
     * @throws PrintException
     */
    private function webscanArrForeach(mixed $arr, bool $init = true): string
    {
        static $str = [];
        static $keystr = '';
        if ($init === true) {
            $str = [];
            $keystr = '';
        }
        if (!is_array($arr)) {
            return (string)$arr;
        }
        foreach ($arr as $key => $val) {
            $keystr = $keystr . $key;
            if (is_array($val)) {
                $this->webscanArrForeach($val, false);
            } else {
                $str[] = $val . $keystr;
            }
        }
        return implode($str);
    }

    /**
     * 攻击检查拦截
     * @param $StrFiltKey
     * @param $StrFiltValue
     * @param $ArrFiltReq
     * @return bool
     * @throws PrintException
     */
    private function webscanStopAttack($StrFiltKey, $StrFiltValue, $ArrFiltReq): bool
    {
        $StrFiltValue = $this->webscanArrForeach($StrFiltValue);
        if (preg_match("/" . $ArrFiltReq . "/is", $StrFiltValue) == 1 || preg_match("/" . $ArrFiltReq . "/is", $StrFiltKey) == 1) {
            return false;
        }
        return true;
    }
}